Add multi-provider search/fetch failover and block empty newsletters from being sent - #8
Merged
Merged
Conversation
Serper credit exhaustion on 2026-06-19 produced seven sectionless newsletters that were still marked complete and queued for delivery. This commit fixes both the resilience and the safety gaps. Resilience: replace the single-provider Serper integration with a deterministic round-robin + failover dispatcher behind a new tools/providers/ package. Serper, Exa, and Tavily each provide search + fetch; Firecrawl and Diffbot add fetch-only. The dispatcher is hidden from the LLM entirely -- the tool signatures (web_search / web_fetch) are unchanged. A provider joins the rotation as soon as its API key env var is set, and drops out automatically when the key is absent or the call errors. Safety: newsletters with no editorial sections (no cited [Read: ...] items) are finalized with status="failed" and never emailed. has_sections() reuses _parse() from the email renderer so the gate matches exactly what would have been sent. Also renames the search tool to web_search for symmetry with web_fetch and updates the two SKILL.md references accordingly.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On 2026-06-19, the Serper credit account hit its hard limit mid-campaign, causing 527 consecutive
search failures. Seven newsletters (EXCL, FORE, MAPI, ERAA, MAPA, SMMA, DSSA) were generated with
no editorial sections, yet were finalized as
completeand queued for delivery. This PR closes boththe resilience gap and the safety gap.
Related issues
Closes #7
Changes
src/agents/tools/providers/(new package): round-robin + failover dispatcher (dispatch.py)and vendor adapters for Serper, Exa, Tavily, Firecrawl, and Diffbot. Each adapter exposes the
same
search()/fetch()interface so the dispatcher is fully hidden from the LLM.src/agents/tools/web_search.py(new, replacessearch.py): routesweb_search()throughSEARCH_PROVIDERS = [serper, exa, tavily]with round-robin + failover. Renamedsearchtoweb_searchfor symmetry withweb_fetch.src/agents/tools/web_fetch.py(updated): routes throughFETCH_PROVIDERS = [serper, exa, tavily, firecrawl, diffbot]. Never raises to the writer -- falls back to a "summarize fromsnippet" message if all providers fail.
src/agents/campaign.py(updated):deliver()checkshas_sections()before rendering orsending. An empty newsletter is finalized as
status="failed"and its recipients are skipped.src/emails/templates/newsletter.py(updated): addshas_sections(), reusing_parse()sothe gate matches the renderer exactly.
section-research/SKILL.md,subject-profile/SKILL.md):searchreferenceschanged to
web_search..env.exampleandCLAUDE.mdupdated with the new provider keys.test_tools.pyfully rewritten for the new module layout; new dispatch engine tests,adapter tests, and multi-provider failover tests added.
test_campaign.pyandtest_email_template.pycover the empty-newsletter guard.How to test
ruff check . && ruff format --check .-- both must pass.cd email-playground && npm run build:templates && cd .. && pytest-- 124 tests, all green.SERPER_API_KEY,EXA_API_KEY) and runpython src/app.py test --email=you@example.com(dry-run). Checkagent_activityto confirmweb_searchcalls are spread across providers.SERPER_API_KEYandEXA_API_KEYat invalid values,run the same command, and confirm the ticker is logged
skipped ... no sections, its newsletterrow is
status="failed", and no email is attempted.